package org.barad.architecture.analyzer.archpad;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.swing.JViewport;
import javax.swing.tree.TreeNode;

import org.barad.architecture.analyzer.archpad.component.ArchPadComponent;
import org.barad.architecture.analyzer.archpad.region.Region;
import org.jgraph.JGraph;
import org.jgraph.event.GraphSelectionEvent;
import org.jgraph.event.GraphSelectionListener;
import org.jgraph.graph.DefaultGraphCell;
import org.jgraph.graph.GraphConstants;
import org.jgraph.graph.GraphLayoutCache;
import org.jgraph.graph.GraphModel;

/**
 * This class is a sub class of {@link JGraph} and 
 * adds some customized behavior to JGraph.
 * 
 * @author svetoslavganov@gmail.com (Svetoslav R. Ganov)
 *
 */
public class CustomizedJGraph extends JGraph {
	public static final long serialVersionUID = 1L;
	private Point2D from;
	private Point2D to;
	private boolean groupComponent;
	private boolean ungroupComponent;
	private boolean saveComponent;
	private boolean expandComponent;
	private boolean collapseComponent;
	private boolean createRegion;
	private ArchPad archPad;
	
	/**
	 * Creates a new {@link CustomizedJGraph} instance.
	 * NOTE: JGraph is using the Mode-View-Connector (MVC) pattern.
	 * 
	 * @param archPad A reference to {@link ArchPad}.
	 * @param model A {@link GraphModel} for representing {@link ArchPad} models.
	 * @param view A view {@link GraphLayoutCache} for visualization of the model.
	 */
	public CustomizedJGraph(ArchPad archPad, GraphModel model, GraphLayoutCache view) {
		super(model, view);
		this.archPad = archPad;
		addGraphSelectionListener();
	}

	/**
	 * Adds customization painting on top of the default one.
	 * This is used for visualizing the enabled regions as 
	 * small colored bar at the top of graph cells. It also
	 * draws the line during connecting two components.
	 */
	@Override 
	public void paint(Graphics g) {
		super.paint(g);
		if (from != null && to != null) {
			Line2D line = new Line2D.Double(from, to);
			Graphics2D g2 = (Graphics2D) g;
			g2.draw(line);
		}
		paintRegions(g);
	}

	/*
	 * Paints the region bars at the top of graph cells.
	 * 
	 * @param g A {@link Graphics} object that performs the drawing.
	 */
	private void paintRegions(Graphics g) {
		JViewport viewPort = (JViewport)getParent();
		Rectangle visiblePart = viewPort.getViewRect();
		Object[] roots = getRoots(visiblePart);	
		List<Object> rootsAndDescendants = Arrays.asList(getDescendants(roots));
		Set<?> visible = getGraphLayoutCache().getVisibleSet();
		for (Object obj: rootsAndDescendants) {
			if (obj != null && obj.getClass().equals(DefaultGraphCell.class) && 
					visible.contains(obj)) {
				DefaultGraphCell cell = (DefaultGraphCell)obj;
				if (hasVisibleChildCell(cell, visible)) {
					continue;
				}
				ArchPadComponent archPadComponent = (ArchPadComponent)cell.getUserObject();
				if (archPadComponent != null) {
					LinkedHashSet<Region> regions = archPadComponent.getRegions();
					if (regions.size() > 0) {
						Rectangle2D bounds = GraphConstants.getBounds(cell.getAttributes());
						fillRectangles(bounds, regions, g);
					}
				}
			}
		}
	}
	
	/*
	 * Fills the region bars given the bounds for each region and the regions. 
	 * @param bounds The bounds of a graph cell.
	 * @param regions The regions.
	 * @param g The {@link Graphics} used for drawing.
	 */
	private void fillRectangles(Rectangle2D bounds, LinkedHashSet<Region> regions, 
		    Graphics g) {
		int width = ((int)bounds.getWidth() - 2) / regions.size();
		int x = (int)bounds.getX() + 1;
		int y = (int)bounds.getY() + 1;
		int height = (int)bounds.getHeight() / 10;
		Iterator<Region> iterator = regions.iterator();
		while (iterator.hasNext()) {
			Region region = iterator.next();
			if (region.isEnabled()) {
				g.setColor(region.getColor());
				g.fillRect(x, y, width, height);
				x = x + width;
			}
		}
	}
	
	/*
	 * Adds an event listener for the selection event in the graph.
	 */
	private void addGraphSelectionListener() {
			addGraphSelectionListener(new GraphSelectionListener() {
			public void valueChanged(GraphSelectionEvent e) {
				setEnabledActions();
			}
		});
	}
	
	/**
	 * Sets the the actions that are enable in the current context.
	 * The context is determined from the user selection in the graph.
	 */
	public void setEnabledActions() {
		disableAllActions();
		Object[] groups = archPad.getComponentManager().getGroups();
		if (groups.length > 0) {
			setExpandCollapseEnabled(groups);
			ungroupComponent = true;
		}
		int selectedCount = getSelectionCount();
		if (selectedCount == 1) {
			if (groups.length == 1) {
				archPad.buildComponentHierarchy();
			} else {
				archPad.resetComponentHierarchy();
			}
			saveComponent = true;
			createRegion = true;
		} else if (selectedCount > 1) {
			saveComponent = true;
			groupComponent = true;
			createRegion = true;
		}
		archPad.setEnabledToolBarWidgets();
	}
	
	/*
	 * Sets if the expanding and collapsing operations are enabled.
	 * 
	 * @param groups The selected groups.
	 */
	private void setExpandCollapseEnabled(Object[] groups) {
		Set<?> visible = getGraphLayoutCache().getVisibleSet();
		for (Object obj: groups) {
			DefaultGraphCell cell = (DefaultGraphCell)obj;
			TreeNode child = cell.getChildAt(0);
			if (visible.contains(child)) {
				collapseComponent = true;
			} else {
				expandComponent = true;
			}
		}
	}
	
	/*
	 * Disables all actions.
	 */
	private void disableAllActions() {
		groupComponent = false;
		ungroupComponent = false;
		saveComponent = false;
		expandComponent = false;
		collapseComponent = false;
		createRegion = false;
	}
	
	/**
	 * The point from which to start drawing a line
	 * connecting two components.This a graph cell.
	 * 
	 * @param from The line starting point.
	 */
	public void setFrom(Point2D from) {
		this.from = from;
	}

	/**
	 * The point to which to stop drawing a line
	 * connecting two components. This is the current
	 * position of the mouse.
	 * 
	 * @param to The line ending point.
	 */
	public void setTo(Point2D to) {
		this.to = to;
	}

	public boolean isCollapseComponent() {
		return collapseComponent;
	}

	/**
	 * Returns if the create region action is available.
	 * 
	 * @return True if the action is available, false otherwise.
	 */
	public boolean isCreateRegion() {
		return createRegion;
	}

	/**
	 * Returns if the expand component action is available.
	 * 
	 * @return True if the action is available, false otherwise.
	 */
	public boolean isExpandComponent() {
		return expandComponent;
	}

	/**
	 * Returns if the group component action is available.
	 * 
	 * @return True if the action is available, false otherwise.
	 */
	public boolean isGroupComponent() {
		return groupComponent;
	}

	public boolean isSaveComponent() {
		return saveComponent;
	}

	/**
	 * Returns if the ungroup component action is available.
	 * 
	 * @return True if the action is available, false otherwise.
	 */
	public boolean isUngroupComponent() {
		return ungroupComponent;
	}
	
	/*
	 * Returns if a graph cell has visible children given the set of
	 * all visible cells.
	 * 
	 * @param cell The cell.
	 * @param visible All visible cells
	 * @return True if the cell has visible children, false otherwise.
	 */
	private boolean hasVisibleChildCell(DefaultGraphCell cell, Set<?> visible) {
		boolean hasVisibleChild = false;
		for (Object obj: cell.getChildren()) {
			if (obj.getClass().equals(DefaultGraphCell.class) 
					&& visible.contains(obj)) {
				hasVisibleChild = true;
				break;
			}
		}
		return hasVisibleChild;
	}
}
